package org.jsux;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;


public class SerialPort
{
    public static final int DATA_BITS_5 = 0;
    public static final int DATA_BITS_6 = 1;
    public static final int DATA_BITS_7 = 2;
    public static final int DATA_BITS_8 = 3;

    public static final int STOP_BITS_1 = 0;
    public static final int STOP_BITS_2 = 1;

    public static final int FLOW_NONE = 0;
    public static final int FLOW_HW = 1;
    public static final int FLOW_SW = 2;

    public static final int PARITY_NONE = 0;
    public static final int PARITY_EVEN = 1;
    public static final int PARITY_ODD = 2;


    static {
        String libName = "SerialPort";
        String libFullName = "SerialPort";
        boolean supported = false;
        boolean loaded = false;

        if(PlatformIdentifier.isWin()) {
            if(PlatformIdentifier.isARM()) {
                //wtf??
            } else if(PlatformIdentifier.isPPC()) {
                //wtf??
            } else if(PlatformIdentifier.is64()) {
                libName += "_x86_64";
                supported = true;
            } else {
                libName += "_x86_32";
                supported = true;
            }
            libFullName = libName+".dll";
        } else if(PlatformIdentifier.isLin()) {
            if(PlatformIdentifier.isARM()) {
            } else if(PlatformIdentifier.isPPC()) {
            } else if(PlatformIdentifier.is64()) {
                libName += "_x86_64";
                supported = true;
            } else {
                libName += "_x86_32";
                supported = true;
            }
            libFullName = "lib"+libName+".so";
        }

        if(supported) {
            try {
                System.loadLibrary(libName);
                loaded = true;
            } catch(UnsatisfiedLinkError e) {
            }

            if(!loaded) {
                try {
                    NativeUtils.loadLibraryFromJar("/native/"+libFullName);
                    loaded = true;
                } catch(Exception e) {
                }
            }

            if(!loaded) {
                System.err.println("Library "+libFullName+" not found!");
            }
        } else {
            System.err.println("Platform not supported!");
        }

    }

    private NativeSerial nativeSerial;
    private long port;

    List<SerialInputStream> inputStreams = new ArrayList<SerialInputStream>();
    List<SerialOutputStream> outputStreams = new ArrayList<SerialOutputStream>();
    boolean nativeClosed = true;
    boolean closed = true;
    boolean open = false;

    public SerialPort() {
        nativeSerial = new NativeSerial();
    }

    public String findPorts()
    {
    	return nativeSerial.findPorts();
    }

    public boolean open(String portName) {
        if(open) return false;

        port = nativeSerial.openPort(portName);
        if(port==0L) return false;

        if(nativeSerial.setNonblock(port)==0) {
        	nativeSerial.closePort(port);
        	return false;
        }

        open = true;
        closed = false;
        nativeClosed = false;

        return true;
    }

    public boolean setupPort(int baud, int dataBits, int stopBits, int parity, int flow) {
    	boolean result = true;
        int ret = 0;

        switch(dataBits)
        {
        case DATA_BITS_5: ret = nativeSerial.setDataBits5(port); break;
        case DATA_BITS_6: ret = nativeSerial.setDataBits6(port); break;
        case DATA_BITS_7: ret = nativeSerial.setDataBits7(port); break;
        case DATA_BITS_8: ret = nativeSerial.setDataBits8(port); break;
        }

        if(ret==0) {
        	result = false;
        }

        ret = 0;
        switch(stopBits)
        {
        case STOP_BITS_1: ret = nativeSerial.setStopBits1(port); break;
        case STOP_BITS_2: ret = nativeSerial.setStopBits2(port); break;
        }

        if(ret==0) {
        	result = false;
        }

        ret = 0;
        switch(parity)
        {
        case PARITY_NONE: ret = nativeSerial.setParityNone(port); break;
        case PARITY_EVEN: ret = nativeSerial.setParityEven(port); break;
        case PARITY_ODD: ret = nativeSerial.setParityOdd(port); break;
        }

        if(ret==0) {
        	result = false;
        }

        ret = 0;
        switch(flow)
        {
        case FLOW_NONE: ret = nativeSerial.setFlowNone(port); break;
        case FLOW_HW: ret = nativeSerial.setFlowHW(port); break;
        case FLOW_SW: ret = nativeSerial.setFlowSW(port); break;
        }

        if(ret==0) {
        	result = false;
        }

        if(nativeSerial.setBaud(port, baud)==0) {
        	result = false;
        }

        nativeSerial.disableDTR(port);
        nativeSerial.disableRTS(port);

        return result;
    }

    public void close() {
        closed = true;

        checkStreams();
    }

    public void checkStreams() {
        boolean allStreamsClosed = true;

        for(SerialInputStream stream: inputStreams) {
            if(!stream.isClosed()) {
                allStreamsClosed = false;
                break;
            }
        }

        if(allStreamsClosed) {
            for(SerialOutputStream stream: outputStreams) {
                if(!stream.isClosed()) {
                    allStreamsClosed = false;
                    break;
                }
            }
        }

        if(closed && allStreamsClosed) {
            synchronized(nativeSerial) {
                nativeSerial.closePort(port);
                nativeClosed = true;
            }
        }
    }

    public InputStream getInputStream() {
        if(closed) return null;

        SerialInputStream stream = new SerialInputStream();
        inputStreams.add(stream);

        return stream;
    }

    public OutputStream getOutputStream() {
        if(closed) return null;

        SerialOutputStream stream = new SerialOutputStream();
        outputStreams.add(stream);

        return stream;
    }

    private void nativeWrite(byte[] buf, int len) throws IOException {
        synchronized(nativeSerial) {
            if(nativeClosed) throw new IOException("Native serial is closed.");
            nativeSerial.writePort(port, buf, len);
        }
    }

    private int nativeRead(byte[] buf, int len) throws IOException {
        synchronized(nativeSerial) {
            if(nativeClosed) throw new IOException("Native serial is closed.");
	    return nativeSerial.readPort(port, buf, len);
        }
    }

    private int nativeFlush() throws IOException {
        synchronized(nativeSerial) {
            if(nativeClosed) throw new IOException("Native serial is closed.");
	    return nativeSerial.flush(port);
        }
    }

    private int nativeAvailable() throws IOException {
        synchronized(nativeSerial) {
            if(nativeClosed) throw new IOException("Native serial is closed.");
	    return nativeSerial.available(port);
        }
    }

    class NativeSerial
    {
       public native String findPorts();
       public native long openPort(String portName);
       public native int setBlock(long ci);
       public native int setNonblock(long ci);
       public native int enableDTR(long ci);
       public native int disableDTR(long ci);
       public native int enableRTS(long ci);
       public native int disableRTS(long ci);
       public native int setParityNone(long ci);
       public native int setParityOdd(long ci);
       public native int setParityEven(long ci);
       public native int setStopBits1(long ci);
       public native int setStopBits2(long ci);
       public native int setDataBits5(long ci);
       public native int setDataBits6(long ci);
       public native int setDataBits7(long ci);
       public native int setDataBits8(long ci);
       public native int setFlowNone(long ci);
       public native int setFlowHW(long ci);
       public native int setFlowSW(long ci);
       public native int setBaud(long ci, int baud);
       public native int flush(long ci);
       public native int closePort(long ci);
       public native int readPort(long ci, byte[] buf, int len);
       public native int writePort(long ci, byte[] buf, int len);
       public native int available(long ci);
    }

	class SerialOutputStream extends OutputStream
	{
	    boolean closed = false;

	    public void close() throws IOException
	    {
	        if(closed) return;

	        closed = true;
	        SerialPort.this.checkStreams();
	    }

	    public boolean isClosed()
	    {
	        return closed;
	    }

	    public void flush() throws IOException
	    {
	        if(closed) throw new IOException();

	        SerialPort.this.nativeFlush();
	    }

	    public void write(int b) throws IOException
	    {
	        if(closed) throw new IOException();

	        byte[] bytes = new byte[1];
	        bytes[0] = (byte)b;
	        SerialPort.this.nativeWrite(bytes,1);
	    }

	    public void write(byte[] b, int off, int len) throws IOException, IndexOutOfBoundsException
	    {
	        if(closed) throw new IOException();
	        if((off<0) || ((off+len)>b.length) ) throw new IndexOutOfBoundsException();

	        byte[] bytes = Arrays.copyOfRange(b, off, off+len);
	        SerialPort.this.nativeWrite(bytes, len);
	    }

	    public void write(byte[] b) throws IOException, IndexOutOfBoundsException
	    {
	        write(b, 0, b.length);
	    }
	}


	class SerialInputStream extends InputStream
	{
	    boolean closed = false;

	    public void close() throws IOException
	    {
	        if(closed) return;

	        closed = true;
	        SerialPort.this.checkStreams();
	    }

	    public boolean isClosed()
	    {
	        return closed;
	    }

	    public int available() throws IOException
	    {
	        if(closed) throw new IOException("Stream closed");
	        return SerialPort.this.nativeAvailable();
	    }

	    public void mark(int readlimit)
	    {
	    }

	    public boolean markSupported()
	    {
	        return false;
	    }

	    public void reset() throws IOException
	    {
	        throw new IOException("mark/reset not supported");
	    }

	    public int read(byte[] b, int off, int len) throws IOException, IndexOutOfBoundsException
	    {
	        if(closed) throw new IOException("Stream closed");
	        if( ((off+len)>b.length) || (off<0) || (len<0) ) throw new IndexOutOfBoundsException();

	        byte[] bytes = new byte[len];
	        int result = SerialPort.this.nativeRead(bytes, len);

	        if(result==-1) throw new IOException("read error");

	        for(int i=0; i<result; i++) {
	        	b[i+off] = bytes[i];
	        }

	        return result;
	    }

	    public int read(byte[] b) throws IOException {
	    	return read(b, 0, b.length);
	    }

	    public int read()
	    {
	        byte[] b = new byte[1];

	        int result;

                try {
                    result = SerialPort.this.nativeRead(b,1);
                } catch(IOException e) {
                    result = -1;
                }

	        if(result==-1) return -1;

	        return b[0];
	    }

	    public long skip(long n) throws IOException
	    {
	        byte[] b = new byte[1024];
	        long result = 0;
	        int rdLen = 0;

	        while(true) {
	        	long toSkip = (n-result);
	        	int len = (toSkip>1024) ? 1024 : (int)toSkip;

	        	rdLen = SerialPort.this.nativeRead(b, len);
	        	if(rdLen==-1) {
	        		if(result==0) result = -1;
	        		break;
	        	}
	        	if(rdLen==0) break;

	        	result+=rdLen;
	        	if(result==n) break;
	        }

	        return result;
	    }
	}

    public static void main(String[] args) {
    	String comPort = "COM15";
//    	String comPort = "ttyS3000";
    	SerialPort sp = new SerialPort();

    	System.out.println("Portas: "+sp.findPorts());

    	if(!sp.open(comPort)) {
    		System.out.println("Erro ao abrir a porta "+comPort);
    		return;
    	}

    	sp.setupPort(2400, DATA_BITS_8, STOP_BITS_1, PARITY_NONE, FLOW_NONE);

    	OutputStream ostream = sp.getOutputStream();
        InputStream istream = sp.getInputStream();

        byte[] outBuffer = new byte[1024];
        byte[] buffer = new byte[1024];
/*
        outBuffer[0] = 'A';
        outBuffer[1] = 'T';
        outBuffer[2] = 'Z';
*/
        outBuffer[0] = '^';
        outBuffer[1] = 'P';
        outBuffer[2] = '0';
        outBuffer[3] = '0';
        outBuffer[4] = '3';
        outBuffer[5] = 'B';
        outBuffer[6] = 'T';
        outBuffer[7] = '2';

        try {
        	while(true)
        	{
        		int respSize = 0;
        		long t1 = System.currentTimeMillis();
    			ostream.write(outBuffer, 0, 8/*3*/);
    	        while(true) {
    	        	try {
    	        		Thread.sleep(10);
    	        	} catch (InterruptedException e) {
    	        		// TODO Auto-generated catch block
    	        		e.printStackTrace();
    	        	}

    	        	int len = istream.read(buffer, 0, 1022);
    	        	if(len==-1) {
    	        		System.out.println("Desconectado!");
    	        		break;
    	        	}
    	        	String str = "";
    	        	for(int i=0; i<len; i++) {
    	        		str += new String(buffer,i,1);
    	        	}
            		System.out.println(str);
            		respSize+=str.length();
            		if(respSize>1){
                		long t2 = System.currentTimeMillis();
//                		System.out.println("tempo: "+(t2-t1));
            			break;
            		}
    	        }
        	}
		} catch (IOException e1) {
			// TODO Auto-generated catch block
			e1.printStackTrace();
		}
    }
}

class PlatformIdentifier
{
    public static boolean isARM() {
        String osArch = System.getProperty("os.arch");
        return osArch.startsWith("arm");
    }

    public static boolean isPPC() {
        String osArch = System.getProperty("os.arch");
        return osArch.contains("ppc");
    }

    public static boolean is64() {
        String osArch = System.getProperty("os.arch");
        return osArch.startsWith("x86_64") || osArch.startsWith("amd64");
    }

    public static boolean isOSX() {
        String osName = System.getProperty("os.name").toLowerCase();
        return osName.startsWith("mac");
    }

    public static boolean isWin() {
        String osName = System.getProperty("os.name").toLowerCase();
        return osName.startsWith("ms") || osName.startsWith("microsoft") || osName.startsWith("windows");
    }

    public static boolean isLin() {
        String osName = System.getProperty("os.name").toLowerCase();
        return osName.startsWith("linux");
    }
}

/**
 * Simple library class for working with JNI (Java Native Interface)
 * 
 * @see http://frommyplayground.com/how-to-load-native-jni-library-from-jar
 *
 * @author Adam Heirnich <adam@adamh.cz>, http://www.adamh.cz
 */
class NativeUtils {
 
    /**
     * Private constructor - this class will never be instanced
     */
    private NativeUtils() {
    }

    /**
     * Loads library from current JAR archive
     * 
     * The file from JAR is copied into system temporary directory and then loaded. The temporary file is deleted after exiting.
     * Method uses String as filename because the pathname is "abstract", not system-dependent.
     * 
     * @param filename The filename inside JAR as absolute path (beginning with '/'), e.g. /package/File.ext
     * @throws IOException If temporary file creation or read/write operation fails
     * @throws IllegalArgumentException If source file (param path) does not exist
     * @throws IllegalArgumentException If the path is not absolute or if the filename is shorter than three characters (restriction of {@see File#createTempFile(java.lang.String, java.lang.String)}).
     */
    public static void loadLibraryFromJar(String path) throws IOException {

        if (!path.startsWith("/")) {
            throw new IllegalArgumentException("The path to be absolute (start with '/').");
        }

        // Obtain filename from path
        String[] parts = path.split("/");
        String filename = (parts.length > 1) ? parts[parts.length - 1] : null;

        // Split filename to prexif and suffix (extension)
        String prefix = "";
        String suffix = null;
        if (filename != null) {
            parts = filename.split("\\.", 2);
            prefix = parts[0];
            suffix = (parts.length > 1) ? "."+parts[parts.length - 1] : null; // Thanks, davs! :-)
        }

        // Check if the filename is okay
        if (filename == null || prefix.length() < 3) {
            throw new IllegalArgumentException("The filename has to be at least 3 characters long.");
        }

        // Prepare temporary file
        File temp = File.createTempFile(prefix, suffix);
        temp.deleteOnExit();

        if (!temp.exists()) {
            throw new FileNotFoundException("File " + temp.getAbsolutePath() + " does not exist.");
        }

        // Prepare buffer for data copying
        byte[] buffer = new byte[1024];
        int readBytes;

        // Open and check input stream
        InputStream is = NativeUtils.class.getResourceAsStream(path);
        if (is == null) {
            throw new FileNotFoundException("File " + path + " was not found inside JAR.");
        }

        // Open output stream and copy data between source file in JAR and the temporary file
        OutputStream os = new FileOutputStream(temp);
        try {
            while ((readBytes = is.read(buffer)) != -1) {
                os.write(buffer, 0, readBytes);
            }
        } finally {
            // If read/write fails, close streams safely before throwing an exception
            os.close();
            is.close();
        }

        // Finally, load the library
        System.load(temp.getAbsolutePath());
    }
}
